/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.scenes.scene2d.ui;

import java.util.List;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.TextBounds;
import com.badlogic.gdx.graphics.g2d.NinePatch;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.utils.Clipboard;
import com.badlogic.gdx.utils.FloatArray;

/** A single-line text input field.
 * <p>
 * The preferred height of a text field is the height of the {@link TextFieldStyle#font} and {@link TextFieldStyle#background}.
 * The preferred width of a text field is 150, a relatively arbitrary size.
 * <p>
 * The text field will copy the currently selected text when ctrl+c is pressed, and paste any text in the clipboard when ctrl+v is
 * pressed. Clipboard functionality is provided via the {@link Clipboard} interface. Currently there are two standard
 * implementations, one for the desktop and one for Android. The Android clipboard is a stub, as copy & pasting on Android is not
 * supported yet.
 * <p>
 * The text field allows you to specify an {@link OnscreenKeyboard} for displaying a softkeyboard and piping all key events
 * generated by the keyboard to the text field. There are two standard implementations, one for the desktop and one for Android.
 * The desktop keyboard is a stub, as a softkeyboard is not needed on the desktop. The Android {@link OnscreenKeyboard}
 * implementation will bring up the default IME.
 * @author mzechner */
public class TextField extends Widget {
	static private final char BACKSPACE = 8;
	static private final char ENTER_DESKTOP = '\r';
	static private final char ENTER_ANDROID = '\n';
	static private final char TAB = '\t';
	static private final char DELETE = 127;
	static private final char BULLET = 149;

	private TextFieldStyle style;
	private String text, messageText;
	private CharSequence displayText;
	private int cursor;
	private Clipboard clipboard;
	private TextFieldListener listener;
	private TextFieldFilter filter;
	private OnscreenKeyboard keyboard = new DefaultOnscreenKeyboard();

	private boolean passwordMode;
	private StringBuilder passwordBuffer;

	private final Rectangle fieldBounds = new Rectangle();
	private final TextBounds textBounds = new TextBounds();
	private final Rectangle scissor = new Rectangle();
	private float renderOffset, textOffset;
	private int visibleTextStart, visibleTextEnd;
	private final FloatArray glyphAdvances = new FloatArray();
	private final FloatArray glyphPositions = new FloatArray();

	private boolean cursorOn = true;
	private float blinkTime = 0.42f;
	private long lastBlink;

	private boolean hasSelection;
	private int selectionStart;
	private float selectionX, selectionWidth;

	private char passwordCharacter = BULLET;

	public TextField (Skin skin) {
		this("", null, skin.getStyle(TextFieldStyle.class), null);
	}

	public TextField (String text, Skin skin) {
		this(text, null, skin.getStyle(TextFieldStyle.class), null);
	}

	/** @param messageText Text to show when empty. May be null. */
	public TextField (String text, String messageText, Skin skin) {
		this(text, messageText, skin.getStyle(TextFieldStyle.class), null);
	}

	public TextField (TextFieldStyle style) {
		this("", null, style, null);
	}

	public TextField (String text, TextFieldStyle style) {
		this(text, null, style, null);
	}

	/** @param messageText Text to show when empty. May be null. */
	public TextField (String text, String messageText, TextFieldStyle style) {
		this(text, messageText, style, null);
	}

	/** @param messageText Text to show when empty. May be null. */
	public TextField (String text, String messageText, TextFieldStyle style, String name) {
		super(name);
		setStyle(style);
		this.clipboard = Clipboard.getDefaultClipboard();
		setText(text);
		this.messageText = messageText;
		width = getPrefWidth();
		height = getPrefHeight();
	}

	public void setStyle (TextFieldStyle style) {
		if (style == null) throw new IllegalArgumentException("style cannot be null.");
		this.style = style;
		invalidateHierarchy();
	}

	public void setPasswordCharacter (char passwordCharacter) {
		this.passwordCharacter = passwordCharacter;
	}

	/** Returns the text field's style. Modifying the returned style may not have an effect until {@link #setStyle(TextFieldStyle)}
	 * is called. */
	public TextFieldStyle getStyle () {
		return style;
	}

	@Override
	public void layout () {
	}

	private void calculateOffsets () {
		float position = glyphPositions.get(cursor);
		float distance = position - Math.abs(renderOffset);
		float visibleWidth = width;
		if (style.background != null) visibleWidth -= style.background.getLeftWidth() + style.background.getRightWidth();

		// check whether the cursor left the left or right side of
		// the visible area and adjust renderoffset.
		if (distance <= 0) {
			if (cursor > 0)
				renderOffset = -glyphPositions.get(cursor - 1);
			else
				renderOffset = 0;
		} else {
			if (distance > visibleWidth) {
				renderOffset -= distance - visibleWidth;
			}
		}

		// calculate first visible char based on render offset
		visibleTextStart = 0;
		textOffset = 0;
		float start = Math.abs(renderOffset);
		int len = glyphPositions.size;
		float startPos = 0;
		for (int i = 0; i < len; i++) {
			if (glyphPositions.items[i] >= start) {
				visibleTextStart = i;
				startPos = glyphPositions.items[i];
				textOffset = glyphPositions.items[visibleTextStart] - start;
				break;
			}
		}

		// calculate last visible char based on visible width and render offset
		visibleTextEnd = Math.min(displayText.length(), cursor + 1);
		for (; visibleTextEnd <= displayText.length(); visibleTextEnd++) {
			if (glyphPositions.items[visibleTextEnd] - startPos > visibleWidth) break;
		}
		visibleTextEnd = Math.max(0, visibleTextEnd - 1);

		// calculate selection x position and width
		if (hasSelection) {
			int minIndex = Math.min(cursor, selectionStart);
			int maxIndex = Math.max(cursor, selectionStart);
			float minX = Math.max(glyphPositions.get(minIndex), glyphPositions.get(visibleTextStart));
			float maxX = Math.min(glyphPositions.get(maxIndex), glyphPositions.get(visibleTextEnd));
			selectionX = minX;
			selectionWidth = maxX - minX;
		}
	}

	@Override
	public void draw (SpriteBatch batch, float parentAlpha) {
		final BitmapFont font = style.font;
		final Color fontColor = style.fontColor;
		final TextureRegion selection = style.selection;
		final NinePatch cursorPatch = style.cursor;

		batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
		float bgLeftWidth = 0;
		if (style.background != null) {
			style.background.draw(batch, x, y, width, height);
			bgLeftWidth = style.background.getLeftWidth();
		}

		float textY = (int)(height / 2 + textBounds.height / 2 + font.getDescent());
		calculateOffsets();

		boolean focused = stage != null && stage.getKeyboardFocus() == this;
		if (focused && hasSelection && selection != null) {
			batch.draw(selection, x + selectionX + bgLeftWidth + renderOffset,
				y + textY - textBounds.height - font.getDescent() / 2, selectionWidth, textBounds.height);
		}

		if (displayText.length() == 0) {
			if (!focused && messageText != null) {
				if (style.messageFontColor != null) {
					font.setColor(style.messageFontColor.r, style.messageFontColor.g, style.messageFontColor.b,
						style.messageFontColor.a * parentAlpha);
				} else
					font.setColor(0.7f, 0.7f, 0.7f, parentAlpha);
				BitmapFont messageFont = style.messageFont != null ? style.messageFont : font;
				font.draw(batch, messageText, x + bgLeftWidth, y + textY);
			}
		} else {
			font.setColor(fontColor.r, fontColor.g, fontColor.b, fontColor.a * parentAlpha);
			font.draw(batch, displayText, x + bgLeftWidth + textOffset, y + textY, visibleTextStart, visibleTextEnd);
		}
		if (focused) {
			blink();
			if (cursorOn && cursorPatch != null) {
				cursorPatch.draw(batch, x + bgLeftWidth + glyphPositions.get(cursor) + renderOffset - 1, y + textY
					- textBounds.height - font.getDescent(), cursorPatch.getTotalWidth(), textBounds.height + font.getDescent() / 2);
			}
		}
	}

	private void updateDisplayText () {
		if (passwordMode && style.font.containsCharacter(passwordCharacter)) {
			if (passwordBuffer == null) passwordBuffer = new StringBuilder(text.length());
			if (passwordBuffer.length() > text.length()) //
				passwordBuffer.setLength(text.length());
			else {
				for (int i = passwordBuffer.length(), n = text.length(); i < n; i++)
					passwordBuffer.append(passwordCharacter);
			}
			displayText = passwordBuffer;
		} else
			displayText = text;
		style.font.computeGlyphAdvancesAndPositions(displayText, glyphAdvances, glyphPositions);
	}

	private void blink () {
		long time = System.nanoTime();
		if ((time - lastBlink) / 1000000000.0f > blinkTime) {
			cursorOn = !cursorOn;
			lastBlink = time;
		}
	}

	@Override
	public boolean touchDown (float x, float y, int pointer) {
		if (pointer != 0) return false;
		if (stage != null) stage.setKeyboardFocus(this);
		keyboard.show(true);
		clearSelection();
		lastBlink = 0;
		cursorOn = false;
		x = x - renderOffset;
		for (int i = 0; i < glyphPositions.size; i++) {
			float pos = glyphPositions.items[i];
			if (pos > x) {
				cursor = Math.max(0, i - 1);
				return true;
			}
		}
		cursor = Math.max(0, glyphPositions.size - 1);
		return true;
	}

	public boolean keyDown (int keycode) {
		final BitmapFont font = style.font;

		if (stage != null && stage.getKeyboardFocus() == this) {
			if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) || Gdx.input.isKeyPressed(Keys.CONTROL_RIGHT)) {
				// paste
				if (keycode == Keys.V) paste();
				// copy
				if (keycode == Keys.C || keycode == Keys.INSERT) copy();
			} else if (Gdx.input.isKeyPressed(Keys.SHIFT_LEFT) || Gdx.input.isKeyPressed(Keys.SHIFT_RIGHT)) {
				// paste
				if (keycode == Keys.INSERT) paste();
				// cut
				if (keycode == Keys.FORWARD_DEL) {
					if (hasSelection) {
						copy();
						delete();
					}
				}
				// selection
				if (keycode == Keys.LEFT) {
					if (!hasSelection) {
						selectionStart = cursor;
						hasSelection = true;
					}
					cursor--;
				}
				if (keycode == Keys.RIGHT) {
					if (!hasSelection) {
						selectionStart = cursor;
						hasSelection = true;
					}
					cursor++;
				}
				if (keycode == Keys.HOME) {
					if (!hasSelection) {
						selectionStart = cursor;
						hasSelection = true;
					}
					cursor = 0;
				}
				if (keycode == Keys.END) {
					if (!hasSelection) {
						selectionStart = cursor;
						hasSelection = true;
					}
					cursor = text.length();
				}

				cursor = Math.max(0, cursor);
				cursor = Math.min(text.length(), cursor);
			} else {
				// cursor movement or other keys (kill selection)
				if (keycode == Keys.LEFT) {
					cursor--;
					clearSelection();
				}
				if (keycode == Keys.RIGHT) {
					cursor++;
					clearSelection();
				}
				if (keycode == Keys.HOME) {
					cursor = 0;
					clearSelection();
				}
				if (keycode == Keys.END) {
					cursor = text.length();
					clearSelection();
				}

				cursor = Math.max(0, cursor);
				cursor = Math.min(text.length(), cursor);
			}

			return true;
		}
		return false;
	}

	private void copy () {
		if (hasSelection) {
			int minIndex = Math.min(cursor, selectionStart);
			int maxIndex = Math.max(cursor, selectionStart);
			clipboard.setContents(text.substring(minIndex, maxIndex));
		}
	}

	private void paste () {
		String content = clipboard.getContents();
		if (content != null) {
			StringBuilder builder = new StringBuilder();
			for (int i = 0; i < content.length(); i++) {
				char c = content.charAt(i);
				if (style.font.containsCharacter(c)) builder.append(c);
			}
			content = builder.toString();
			text = text.substring(0, cursor) + content + text.substring(cursor, text.length());
			updateDisplayText();
			cursor += content.length();
		}
	}

	private void delete () {
		int minIndex = Math.min(cursor, selectionStart);
		int maxIndex = Math.max(cursor, selectionStart);
		text = (minIndex > 0 ? text.substring(0, minIndex) : "")
			+ (maxIndex < text.length() ? text.substring(maxIndex, text.length()) : "");
		updateDisplayText();
		cursor = minIndex;
		clearSelection();
	}

	public boolean keyTyped (char character) {
		final BitmapFont font = style.font;

		if (stage != null && stage.getKeyboardFocus() == this) {
			if (character == BACKSPACE && (cursor > 0 || hasSelection)) {
				if (!hasSelection) {
					text = text.substring(0, cursor - 1) + text.substring(cursor);
					updateDisplayText();
					cursor--;
				} else {
					delete();
				}
			}
			if (character == DELETE) {
				if (cursor < text.length() || hasSelection) {
					if (!hasSelection) {
						text = text.substring(0, cursor) + text.substring(cursor + 1);
						updateDisplayText();
					} else {
						delete();
					}
				}
				return true;
			}
			if (character != ENTER_DESKTOP && character != ENTER_ANDROID) {
				if (filter != null && !filter.acceptChar(this, character)) return true;
			}
			if (character == TAB || character == ENTER_ANDROID)
				next(Gdx.input.isKeyPressed(Keys.SHIFT_LEFT) || Gdx.input.isKeyPressed(Keys.SHIFT_RIGHT));
			if (font.containsCharacter(character)) {
				if (!hasSelection) {
					text = text.substring(0, cursor) + character + text.substring(cursor, text.length());
					updateDisplayText();
					cursor++;
				} else {
					int minIndex = Math.min(cursor, selectionStart);
					int maxIndex = Math.max(cursor, selectionStart);

					text = (minIndex > 0 ? text.substring(0, minIndex) : "")
						+ (maxIndex < text.length() ? text.substring(maxIndex, text.length()) : "");
					cursor = minIndex;
					text = text.substring(0, cursor) + character + text.substring(cursor, text.length());
					updateDisplayText();
					cursor++;
					clearSelection();
				}
			}
			if (listener != null) listener.keyTyped(this, character);
			return true;
		} else
			return false;
	}

	/** Focuses the next TextField. If none is found, the keyboard is hidden. Does nothing if the text field is not in a stage.
	 * @param up If true, the TextField with the same or next smallest y coordinate is found, else the next highest. */
	public void next (boolean up) {
		if (stage == null) return;
		TextField textField = findNextTextField(stage.getActors(), null, up);
		if (textField != null)
			stage.setKeyboardFocus(textField);
		else
			Gdx.input.setOnscreenKeyboardVisible(false);
	}

	private TextField findNextTextField (List<Actor> actors, TextField best, boolean up) {
		for (int i = 0, n = actors.size(); i < n; i++) {
			Actor actor = actors.get(i);
			if (actor instanceof TextField) {
				if (actor == this) continue;
				if (actor.y == y) {
					if (best == null && actor.x >= x ^ up) best = (TextField)actor;
				} else if (actor.y < y ^ up && (best == null || actor.y - y > best.y - y ^ up)) {
					best = (TextField)actor;
				}
			}
			if (actor instanceof Group) best = findNextTextField(((Group)actor).getActors(), best, up);
		}
		return best;
	}

	/** @param listener May be null. */
	public void setTextFieldListener (TextFieldListener listener) {
		this.listener = listener;
	}

	/** @param filter May be null. */
	public void setTextFieldFilter (TextFieldFilter filter) {
		this.filter = filter;
	}

	/** @return May be null. */
	public String getMessageText () {
		return messageText;
	}

	/** Sets the text that will be drawn in the text field if no text has been entered.
	 * @parma messageText May be null. */
	public void setMessageText (String messageText) {
		this.messageText = messageText;
	}

	public void setText (String text) {
		if (text == null) throw new IllegalArgumentException("text cannot be null.");

		BitmapFont font = style.font;

		StringBuffer buffer = new StringBuffer();
		for (int i = 0; i < text.length(); i++) {
			char c = text.charAt(i);
			if (font.containsCharacter(c)) buffer.append(c);
		}

		this.text = buffer.toString();
		updateDisplayText();
		cursor = 0;
		clearSelection();

		textBounds.set(font.getBounds(displayText));
		textBounds.height -= font.getDescent() * 2;
		font.computeGlyphAdvancesAndPositions(displayText, glyphAdvances, glyphPositions);
	}

	/** @return Never null, might be an empty string. */
	public String getText () {
		return text;
	}

	/** Sets the selected text. */
	public void setSelection (int selectionStart, int selectionEnd) {
		if (selectionStart < 0) throw new IllegalArgumentException("selectionStart must be >= 0");
		if (selectionEnd < 0) throw new IllegalArgumentException("selectionEnd must be >= 0");
		selectionStart = Math.min(text.length(), selectionStart);
		selectionEnd = Math.min(text.length(), selectionEnd);
		if (selectionEnd == selectionStart) {
			clearSelection();
			return;
		}
		if (selectionEnd < selectionStart) {
			int temp = selectionEnd;
			selectionEnd = selectionStart;
			selectionStart = temp;
		}

		hasSelection = true;
		this.selectionStart = selectionStart;
		cursor = selectionEnd;
	}

	public void clearSelection () {
		hasSelection = false;
	}

	/** Sets the cursor position and clears any selection. */
	public void setCursorPosition (int cursorPosition) {
		if (cursorPosition < 0) throw new IllegalArgumentException("cursorPosition must be >= 0");
		clearSelection();
		cursor = Math.min(cursorPosition, text.length());
	}

	public int getCursorPosition () {
		return cursor;
	}

	/** Default is an instance of {@link DefaultOnscreenKeyboard}. */
	public OnscreenKeyboard getOnscreenKeyboard () {
		return keyboard;
	}

	public void setOnscreenKeyboard (OnscreenKeyboard keyboard) {
		this.keyboard = keyboard;
	}

	public void setClipboard (Clipboard clipboard) {
		this.clipboard = clipboard;
	}

	public float getPrefWidth () {
		return 150;
	}

	public float getPrefHeight () {
		float prefHeight = textBounds.height;
		if (style.background != null) prefHeight += style.background.getBottomHeight() + style.background.getTopHeight();
		return prefHeight;
	}

	/** If true, the text in this text field will be shown as bullet characters. The font must have character 149 or this will have
	 * no affect. */
	public void setPasswordMode (boolean passwordMode) {
		this.passwordMode = passwordMode;
	}

	/** Interface for listening to typed characters.
	 * @author mzechner */
	static public interface TextFieldListener {
		public void keyTyped (TextField textField, char key);
	}

	/** Interface for filtering characters entered into the text field.
	 * @author mzechner */
	static public interface TextFieldFilter {
		/** @param textField
		 * @param key
		 * @return whether to accept the character */
		public boolean acceptChar (TextField textField, char key);

		static public class DigitsOnlyFilter implements TextFieldFilter {
			@Override
			public boolean acceptChar (TextField textField, char key) {
				return Character.isDigit(key);
			}

		}
	}

	/** An interface for onscreen keyboards. Can invoke the default keyboard or render your own keyboard!
	 * @author mzechner */
	static public interface OnscreenKeyboard {
		public void show (boolean visible);
	}

	/** The default {@link OnscreenKeyboard} used by all {@link TextField} instances. Just uses
	 * {@link Input#setOnscreenKeyboardVisible(boolean)} as appropriate. Might overlap your actual rendering, so use with care!
	 * @author mzechner */
	static public class DefaultOnscreenKeyboard implements OnscreenKeyboard {
		@Override
		public void show (boolean visible) {
			Gdx.input.setOnscreenKeyboardVisible(visible);
		}
	}

	/** The style for a text field, see {@link TextField}.
	 * @author mzechner */
	static public class TextFieldStyle {
		/** Optional. */
		public NinePatch background, cursor;
		public BitmapFont font;
		public Color fontColor;
		/** Optional. */
		public TextureRegion selection;
		/** Optional. */
		public BitmapFont messageFont;
		/** Optional. */
		public Color messageFontColor;

		public TextFieldStyle () {
		}

		public TextFieldStyle (BitmapFont font, Color fontColor, BitmapFont messageFont, Color messageFontColor, NinePatch cursor,
			TextureRegion selection, NinePatch background) {
			this.messageFont = messageFont;
			this.messageFontColor = messageFontColor;
			this.background = background;
			this.cursor = cursor;
			this.font = font;
			this.fontColor = fontColor;
			this.selection = selection;
		}
	}
}
